Skip to content

Python: feat: cross-session origin attribution on context messages#5916

Closed
finnoybu wants to merge 2 commits into
microsoft:mainfrom
aegis-initiative:feat/memory-origin-session-attribution
Closed

Python: feat: cross-session origin attribution on context messages#5916
finnoybu wants to merge 2 commits into
microsoft:mainfrom
aegis-initiative:feat/memory-origin-session-attribution

Conversation

@finnoybu

Copy link
Copy Markdown

Summary

Add an optional origin_session_id parameter to SessionContext.extend_messages that propagates into the existing _attribution payload on Message.additional_properties. Populate it from the harness memory consolidation pipeline when injecting topics that include contributions from sessions other than the current one. Add a self-contained sample observer demonstrating subscription via the standard ContextProvider pipeline.

Companion to #5914 for design discussion. Submitted as a draft — per CONTRIBUTING.md ("DON'T make new APIs without filing an issue and discussing with us first"), I'd appreciate maintainer signal on the API-shape questions in #5914 before this goes to ready-for-review.

Motivation

Dai et al., Stateful Agent Backdoor (arXiv:2605.06158, preprint May 2026) documents a cross-session attack class that uses persistent memory to chain sub-backdoors across session boundaries under permission isolation. The paper specifically surveys MAF in §6.1 / Table 10 as satisfying both threat-model preconditions.

The paper's primary suggested defense is cross-session behavioral pattern analysis (§6.3). MAF's existing ContextProvider pipeline is the natural enforcement point — but downstream observers can't see the cross-session signal today because the framework doesn't expose which session injected content originated from. This PR closes that gap.

See #5914 for the full motivation, the architecture analysis, and the open API questions.

Changes

  • python/packages/core/agent_framework/_sessions.pySessionContext.extend_messages gains an optional origin_session_id: str | None = None keyword parameter. When non-None, it's added to the existing _attribution dict alongside source_id / source_type. Default behavior is unchanged.
  • python/packages/core/agent_framework/_harness/_memory.pyMemoryContextProvider.before_run now computes a cross_session_origin from selected_topics[*].session_ids and passes it through extend_messages when injecting the consolidated memory block. The MemoryTopicRecord.session_ids field already tracked this provenance data; we just route it to the attribution layer.
  • python/samples/02-agents/context_providers/cross_session_observer.py — new self-contained sample. A CrossSessionObserver(ContextProvider) walks accumulated context messages in before_run, finds those whose attribution carries an origin_session_id different from the current session, and invokes a user-supplied callback. The sample's main() constructs SessionContext directly (no LLM credentials required) and demonstrates both the same-session silence case and the cross-session detection case.
  • python/samples/02-agents/context_providers/README.md — table entry + prerequisites section for the new sample.
  • Teststests/core/test_sessions.py gains four cases (origin omitted by default; origin recorded on attribution; origin works with string source_id; origin works with provider-object source). tests/core/test_harness_memory.py gains two cases (harness marks cross-session origin; harness omits origin when only the current session contributes).

Backward compatibility

Strictly additive:

  • The _attribution dict shape is extended with one optional key (origin_session_id); the existing source_id / source_type fields are unchanged.
  • Callers that don't pass origin_session_id see the historical attribution dict exactly (verified by test_extend_messages_origin_session_id_default_omits_field).
  • No new public types; no changes to abstract ContextProvider / HistoryProvider interfaces; no behavioral change to any provider that doesn't opt in.
  • Observers that don't know about origin_session_id ignore it and see today's behavior.

Test plan

  • uv run pytest packages/core/tests/core/test_sessions.py packages/core/tests/core/test_harness_memory.py — all 77 tests pass locally (Python 3.14, Windows). The six new tests cover the parameter, default, harness cross-session, harness same-session, string-source, and provider-object-source cases.
  • uv run pytest packages/core/tests/ — full core suite: 2774 passed, 27 skipped, 2 xfailed, 0 newly failing. (One pre-existing unrelated test_telemetry.py::test_detect_hosted_fallback_import_error failure on this Python 3.14 / Windows environment, reproducible against main without my changes.)
  • uv run ruff check + ruff format --check clean on all six modified files.
  • uv run pyright clean (0 errors, 0 warnings, 0 informations) on core sources, tests, and the sample.
  • Sample runs end-to-end and produces the expected detection output:
    --- Same-session case complete (no detections expected above) ---
    [cross-session detected] source='memory_provider' origin_session='session-A' current_session='session-B' preview='Remember: API key for prod is sk-7f3a... (from session-A).'
    --- Cross-session case complete (one detection expected above) ---
    

Open API questions (from #5914 — please weigh in)

  1. Attribution key naming. origin_session_id vs source_session_id?
  2. Threading mechanism. Keyword parameter on extend_messages (current proposal) vs providers setting the field directly on msg.additional_properties["_attribution"] after call?
  3. Built-in session-scoped providers (InMemoryHistoryProvider, FileHistoryProvider) — should they populate the field for consistency, even though it would always equal session_id?
  4. Typed shape. In-place dict extension (current) vs introducing a typed MessageAttribution dataclass.

Happy to revise based on any direction.

Related


Surfaced during independent audit conducted by @finnoybu (Ken Tannenbaum, AEGIS Initiative); [MEDIUM, python/packages/core].

Add an optional origin_session_id parameter to SessionContext.extend_messages
that propagates into the existing _attribution payload on
Message.additional_properties. Downstream context observers can use it to
detect when a provider injects content stored under a different session than
the requesting one.

Populate the field from the harness memory consolidation pipeline
(_harness/_memory.py) when injected topics include contributions from
sessions other than the current one. Add a self-contained sample observer
under samples/02-agents/context_providers/cross_session_observer.py
demonstrating how to subscribe to the signal.

Backward-compatible: omitting the parameter preserves the existing
attribution shape exactly. Tests added in test_sessions.py and
test_harness_memory.py cover the new parameter, the harness cross-session
case, and the same-session case.

Motivated by Dai et al., Stateful Agent Backdoor (arXiv:2605.06158, May
2026), which specifically surveys MAF in section 6.1 / Table 10.
See microsoft#5914 for design discussion.

Surfaced during independent audit conducted by @finnoybu (Ken Tannenbaum, AEGIS Initiative); [MEDIUM, python/packages/core].
Copilot AI review requested due to automatic review settings May 18, 2026 10:15
@moonbox3 moonbox3 added documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python labels May 18, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an optional cross-session provenance signal to Python context-message attribution so governance/observer providers can detect when injected context originated from a different session_id (e.g., consolidated memory).

Changes:

  • Extend SessionContext.extend_messages() with a keyword-only origin_session_id that (when provided) is added to Message.additional_properties["_attribution"].
  • Update the harness memory context provider to compute a cross-session origin from MemoryTopicRecord.session_ids and pass it through when injecting the consolidated memory block.
  • Add a new self-contained sample observer plus tests covering default/annotated behavior and harness propagation.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
python/packages/core/agent_framework/_sessions.py Adds origin_session_id to extend_messages() and documents the new attribution field.
python/packages/core/agent_framework/_harness/_memory.py Threads cross-session provenance into the injected memory block attribution.
python/packages/core/tests/core/test_sessions.py Adds tests for default omission and explicit attribution of origin_session_id.
python/packages/core/tests/core/test_harness_memory.py Adds tests verifying harness memory marks cross-session origin and omits it for same-session topics.
python/samples/02-agents/context_providers/cross_session_observer.py New sample ContextProvider observer that detects cross-session attributed context messages.
python/samples/02-agents/context_providers/README.md Documents the new sample and its prerequisites.
Comments suppressed due to low confidence (1)

python/packages/core/agent_framework/_sessions.py:263

  • extend_messages() currently uses additional_properties.setdefault("_attribution", attribution), so if a caller passes a Message that already has an _attribution dict, the new origin_session_id parameter will be ignored (and cannot be added/merged). Consider merging origin_session_id into an existing mapping when it’s missing (without overwriting existing keys), or update the docstring to make it explicit that pre-attributed messages won’t get the origin marker.
        copied: list[Message] = []
        for message in messages:
            msg_copy = copy.copy(message)
            msg_copy.additional_properties = dict(message.additional_properties)
            msg_copy.additional_properties.setdefault("_attribution", attribution)
            copied.append(msg_copy)

Comment on lines +1315 to +1317
# block accordingly. See the ``CrossSessionObserver`` sample under
# ``samples/governance/cross_session_observer`` for an example
# subscriber, and the attribution mechanism on
Comment on lines +1320 to +1327
cross_session_origin: str | None = None
for record in selected_topics:
for contributor in record.session_ids:
if contributor and contributor != current_session_id:
cross_session_origin = contributor
break
if cross_session_origin is not None:
break
Comment on lines +242 to +247
value is exposed under ``additional_properties["_attribution"]
["origin_session_id"]`` so downstream context observers can
detect cross-session content for governance, audit, or
behavioral-analysis purposes. Omit (default) when content
originates in the current session — absence of the field is
semantically equivalent to "no origin information."
@finnoybu finnoybu marked this pull request as ready for review June 8, 2026 13:06
@moonbox3

moonbox3 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

@finnoybu thanks for your contribution. Please address Copilot's PR feedback. Once done we can move forward with getting this into main.

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/core/agent_framework
   _sessions.py3893391%102–104, 106–107, 124–125, 127–129, 206–207, 315, 576–580, 622, 625, 659, 708, 712, 722, 855, 871, 1004, 1018–1019, 1042, 1064, 1074, 1116
packages/core/agent_framework/_harness
   _memory.py78111785%75–76, 92, 99, 151, 164, 171–173, 196, 216, 225–229, 292, 298, 333, 414, 422, 424, 428, 431, 435, 530, 542, 560–561, 572–573, 584, 699, 716, 725, 740, 768, 771–773, 783, 805–808, 857, 867, 870, 872, 892, 902, 907, 910, 922, 990, 992, 994, 996, 998, 1000, 1070, 1087, 1115, 1144, 1152–1154, 1156, 1210, 1236–1239, 1245, 1407, 1410, 1421–1422, 1426, 1429–1431, 1437, 1441, 1446, 1450, 1455, 1459, 1467–1468, 1519, 1522–1525, 1532–1533, 1561, 1565–1573, 1590, 1616–1617, 1620–1621, 1627, 1629, 1634, 1639, 1645
TOTAL38417439188% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
7702 34 💤 0 ❌ 0 🔥 2m 1s ⏱️

@moonbox3

Copy link
Copy Markdown
Contributor

#7041 supersedes this PR and incorporates the feedback.

@moonbox3 moonbox3 closed this Jul 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants